<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <p>姓名：<input type="text" v-model="name" /></p>
      <p>学号：<input type="text" v-model="number" /></p>
      <p><span>学号：</span> <span>{{ number }}</span></p>
      <p><span>computed实现：</span> <span>{{ getStudent }}</span></p>

      <p>
        <button v-on:click="setData">事件绑定</button>
      </p>
    </div>
    <script>
      const defineProp = function (obj, key, value) {
        observe(value)

        /*
         * 预先为每一个不同属性创建一个独有的dep
         */
        const dep = new Dep()
        Object.defineProperty(obj, key, {
          get: function () {
            /*
             * 根据不同的属性创建且只在创建Watcher时调用
             */
            if (Dep.target) {
              dep.targetAddDep()
            }
            return value
          },
          set: function (newVal) {
            if (newVal !== value) {
              /*
               * 这里的赋值操作，是以便于get  方法中返回value,因为都是赋值后马上就会调用get方法
               */
              value = newVal
              /*
               * 通知监听的属性的所有订阅者
               */
              dep.notify()
            }
          },
        })
      }

      const observe = function (obj) {
        if (!obj || typeof obj !== 'object') return

        Object.keys(obj).forEach(function (key) {
          defineProp(obj, key, obj[key])
        })
      }
    </script>

    <script>
      let UUID = 0
      function Dep() {
        this.id = UUID++
        // 存放当前属性的所有的监听watcher
        this.subs = []
      }
      Dep.prototype.addSub = function (watcher) {
        this.subs.push(watcher)
      }
      // 目的是将当前 dep 实例 传入 watcher
      Dep.prototype.targetAddDep = function () {
        // 这里 this 是实例化后的 dep
        Dep.target.addDep(this)
      }
      Dep.prototype.notify = function () {
        // 触发当前属性的所有 watcher
        this.subs.forEach((_watcher) => {
          _watcher.update()
        })
      }
      Dep.target = null
    </script>

    <script>
      function Watcher(vm, prop, callback) {
        this.vm = vm
        this.prop = prop
        this.callback = callback
        this.depId = {}
        this.value = this.pushWatcher()
      }

      Watcher.prototype = {
        update: function () {
          /* 更新值的变化 */
          const value = this.vm[this.prop]
          const oldValue = this.value
          if (value !== oldValue) {
            this.value = value
            this.callback(value)
          }
        },
        // 目的是接收 dep 实例，用于将当前watcher实例放入 subs
        addDep: function (dep) {
          if (!this.depId.hasOwnProperty(dep.id)) {
            dep.addSub(this)
            this.depId[dep.id] = dep.id
          } else {
            console.log('already exist')
          }
        },
        pushWatcher: function () {
          // 存贮订阅器
          Dep.target = this
          // 触发对象的get监听，将上面赋值给 target 的this 加入到subs
          var value = this.vm[this.prop]
          // 加入完后就删除
          Dep.target = null
          return value
        },
      }
    </script>

    <script>
      function Compile(vm) {
        this._vm = vm
        this._el = vm._el
        this.methods = vm._methods
        this.fragment = null
        this.init()
      }
      Compile.prototype = {
        init: function () {
          this.fragment = this.createFragment()
          this.compileNode(this.fragment)

          // 当代码片段中的内容编译完了之后，插入DOM中
          this._el.appendChild(this.fragment)
        },

        // 根据真实的DOM节点，创建文档片段
        createFragment: function () {
          const fragment = document.createDocumentFragment()
          let child = this._el.firstChild
          while (child) {
            // 将节点加入到文档片段中后，该节点会被从原来的位置删除，相当于移动节点位置
            fragment.appendChild(child)
            child = this._el.firstChild
          }

          return fragment
        },

        compileNode: function (fragment) {
          let childNodes = fragment.childNodes
          ;[...childNodes].forEach((node) => {
            if (this.isElementNode(node)) {
              this.compileElementNode(node)
            }

            let reg = /\{\{(.*)\}\}/
            // 获取节点下的所有文本内容
            let text = node.textContent

            // 判断是否已是纯文本节点，且文本内容是否有{{}}
            if (this.isTextNode(node) && reg.test(text)) {
              let prop = reg.exec(text)[1].trim()
              this.compileText(node, prop)
            }

            if (node.childNodes && node.childNodes.length) {
              // 递归编译子节点
              this.compileNode(node)
            }
          })
        },

        compileElementNode: function (element) {
          // 获取属性，只有元素节点有如下方法
          let nodeAttrs = element.attributes
          ;[...nodeAttrs].forEach((attr) => {
            let name = attr.name

            if (this.isDirective(name)) {
              /*
               * v-model 放在可接受input事件的标签上
               */
              let prop = attr.value
              if (name === 'v-model') {
                /*
                 * 获取到的value 即为需要绑定的data
                 */
                this.compileModel(element, prop)
              } else if (this.isEvent(name)) {
                /*
                 * 绑定事件
                 */
                this.bindEvent(element, name, prop)
              }
            }
          })
        },

        compileModel: function (element, prop) {
          let val = this._vm[prop]
          this.updateElementValue(element, val)
          new Watcher(this._vm, prop, (value) => {
            this.updateElementValue(element, value)
          })

          element.addEventListener('input', (event) => {
            let newValue = event.target.value
            if (val === newValue) return
            this._vm[prop] = newValue
          })
        },

        compileText: function (textNode, prop) {
          let text = ''
          if (/\./.test(prop)) {
            var props = prop.split('.')
            text = this._vm[props[0]][props[1]]
          } else {
            text = this._vm[prop]
          }

          this.updateText(textNode, text)

          console.log(text)

          new Watcher(this._vm, prop, (value) => {
            this.updateText(textNode, value)
          })
        },

        bindEvent: function (element, name, prop) {
          var eventType = name.split(':')[1]
          var fn = this._vm._methods[prop]
          element.addEventListener(eventType, fn.bind(this._vm))
        },

        /*
         * 判断属性是否为指令
         */
        isDirective: function (text) {
          return /v-/.test(text)
        },

        isEvent: function (text) {
          return /v-on/.test(text)
        },

        isElementNode: function (node) {
          // 元素节点返回1 文本节点(元素或属性中的文字)3 属性节点2(被废弃)
          return node.nodeType === 1
        },

        isTextNode: function (node) {
          return node.nodeType === 3
        },

        updateElementValue: function (element, value) {
          element.value = value || ''
        },

        updateText: function (textNode, value) {
          textNode.textContent = value || ''
        },
      }
    </script>

    <script>
      function FakeVue(options) {
        this._data = options.data
        this._methods = options.methods
        this._computed = options.computed
        this._el = document.querySelector(options.el)

        // 将 _data 中的属性代理到外层的vm上,这里只代理了_data的第一层属性
        Object.keys(this._data).forEach((key) => {
          this._proxyData(key)
        })
        this._initComputed()
        this._init()
      }
      FakeVue.prototype._init = function () {
        // 开始递归监听对象的所有属性，直到属性值为值类型
        observe(this._data)
        new Compile(this)
      }
      FakeVue.prototype._proxyData = function (key) {
        Object.defineProperty(this, key, {
          get: function () {
            return this._data[key]
          },
          set: function (value) {
            this._data[key] = value
          },
        })
      }
      FakeVue.prototype._initComputed = function () {
        // 简单的实现: 将值挂载到跟上即可
        const computed = this._computed
        Object.keys(computed).forEach((v) => {
          Object.defineProperty(this, v, {
            get: computed[v],
            set: function (value) {},
          })
        })
      }
    </script>
    <script>
      try {
        let vm = new FakeVue({
          el: '#app',
          data: {
            name: 'warren',
            number: '10011',
            score: {
              math: 90,
            },
          },

          computed: {
            getStudent: function () {
              return `${this.name}：学号是 ${this.number}`
            },
          },

          methods: {
            // 通过在compile中给元素绑定事件实现
            setData: function () {
              alert('name：' + this.name)
            },
          },
        })
      } catch (error) {
        console.error(error)
      }
    </script>
  </body>
</html>
